package hw3.proxy.core;

import hw3.exception.StdErrReporterException;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;

import org.apache.http.HttpException;
import org.apache.http.HttpResponse;


/**
 * The main proxy class.
 *  
 * @author Adam Levi
 * @author Marina Skarbovsky
 *
 */
public class ProxyServer implements Runnable
{
	/**
	 * Default constructor.
	 * <p>
	 * handles the following:
	 * <ol>
	 * <li>Loads the properties from the properties file</li>
	 * <li>Initialize the members</li>
	 * <li>Sets the system property: "http.nonProxyHosts"</li>
	 * </ol>
	 * since the API does not allow throwing checked exceptions then in any case
	 * of an error a <b>Runtime Exception is thrown</b>.
	 * </p>
	 */
	public ProxyServer()
	{
		// loading properties file
		int dataBasePort, proxyPort;
		String user, password, host, db, nonProxyHosts, tokens, newTokens;
		Properties props = new Properties();
		try 
		{
			// loading file
			props.load(new FileInputStream("proxy.properties"));

			// loading properties
			user = props.getProperty("DBServer.user");
			password = props.getProperty("DBServer.password");
			host = props.getProperty("DBServer.host");
			dataBasePort = Integer.parseInt(props.getProperty("DBServer.port"));
			db = props.getProperty("DBServer.database");
			proxyPort = Integer.parseInt(props.getProperty("proxy.port"));
			nonProxyHosts = props.getProperty("http.nonProxyHosts");
			tokens = props.getProperty("proxy.tokens");
			newTokens = props.getProperty("proxy.newTokens");
		} 
		catch (Exception e) 
		{
			StdErrReporterException.reportErrorFromException(
					ProxyServer.class.getName(), StdErrReporterException.UNABLE_TO_LOAD_PROP_FILE, e);
			throw new RuntimeException(StdErrReporterException.UNABLE_TO_LOAD_PROP_FILE); 
		}
		Hashtable<String, String> tokenHash = getTokenMapping(tokens, newTokens, ' ');
		
		// sets the system property http.nonProxyHosts
		try
		{
			System.setProperty("http.nonProxyHosts", nonProxyHosts);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ProxyServer.class.getName(), "Unable to set the system property http.nonProxyHosts", e);
			throw new RuntimeException("Unable to set the system property http.nonProxyHosts"); 
		}
		
		// creating members
		try
		{
			m_ResourceManager = new ResourceManager(
					host, dataBasePort, db, user, password,
					(tokenHash != null ? tokenHash.entrySet() : null));
			m_ClientHandler = new ClientConnHandler(proxyPort);
			m_HttpMsgHandler = new HttpMsgHandler(m_ResourceManager);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ProxyServer.class.getName(), StdErrReporterException.INIT_FAILED, e);
			throw new RuntimeException(StdErrReporterException.INIT_FAILED); 
		}
	}

	/**
	 * Starts the proxy, that is, listening and serving requests.
	 */
	public void start() 
	{
		try
		{
			m_ClientHandler.initialize();
		}
		catch(IOException e)
		{
			StdErrReporterException.reportErrorFromException(
					this.toString(), 
					StdErrReporterException.NETWORKING_ERROR,
					e);
			return;
		}
		while(true)
		{
			try 
			{
				HttpRequestToHandle request = m_ClientHandler.getHttpRequestToHandle();
				HttpResponse response = m_HttpMsgHandler.handleRequest(request);
				m_ClientHandler.sendResponse(response, request.getConnectionToClient());
				request.finalize();
			} 
			catch (HttpException e) 
			{
				StdErrReporterException.reportErrorFromException(
						this.toString(), 
						StdErrReporterException.HTTP_ERROR,
						e);
				continue;
			} 
			catch (IOException e)
			{
				StdErrReporterException.reportErrorFromException(
						this.toString(), 
						StdErrReporterException.NETWORKING_ERROR,
						e);
				continue;
			}
		}
	}

	/**
	 * Constructs the tables of the cache.
	 */
	public void constructCacheTables() 
	{
		doMethod(MethodToInvoke.CONSTRUCT_CACHE_TABLES, null);
	}

	/**
	 * Deletes the tables of the cache.
	 */
	public void deleteCacheTables() 
	{
		doMethod(MethodToInvoke.DELETE_CACHE_TABLES, null);
	}

	/**
	 * Clears the content of the cache (the tables are NOT deleted).
	 */
	public void clearCache() 
	{
		doMethod(MethodToInvoke.CLEAR_CACHE, null);
	}

	/**
	 * Receives an SQL query, poses it over the tables of the cache and prints the results.
	 * @param query an SQL query to be posed over the tables of the cache.
	 */
	public void poseQuery(String query) 
	{
		doMethod(MethodToInvoke.POSE_QUERY, query);
	}


	/**
	 * Termination. This method closes resources like statements, connections,
	 * This method DOES NOT DELETE any table or table data.
	 */
	public void terminate() 
	{
		// terminating the Resource manager
		m_ResourceManager.terminate();
	}

	
// disabled main	
//	public static void main(String args[]) throws Exception
//	{
//		ProxyServer proxy = new ProxyServer();
//		proxy.start();
//	}

	Hashtable<String, String> getTokenMapping(String tokens, String newTokens, Character delimiter)
	{

		if (tokens == null || newTokens == null)
		{
			StdErrReporterException.reportError(
					ProxyServer.class.getName(),
					StdErrReporterException.TOKEN_STRINGS_ERROR);
			return null;
		}
		String[] tokenArray = tokens.split(delimiter.toString());
		String[] newTokenArray = newTokens.split(delimiter.toString());
		int n = tokenArray.length;
		if (newTokenArray.length != n)
		{
			StdErrReporterException.reportError(
					ProxyServer.class.getName(),
					StdErrReporterException.TOKEN_STRINGS_ERROR);
			return null;	
		}
		
		Hashtable<String, String> resHash = new Hashtable<String, String>();
		
		for (int i = 0; i < n; i++)
		{
			resHash.put(tokenArray[i], newTokenArray[i]);
		}
		
		return resHash;
	}
	
	/**
	 * invokes a specific method. does not use reflection to reduce overhead.
	 * @param meth the method to invoke
	 * @param arg the argument (if exists) for the method
	 */
	void doMethod(MethodToInvoke meth, Object arg)
	{
		try
		{
			switch(meth)
			{
				case CONSTRUCT_CACHE_TABLES: m_ResourceManager.constructCacheTables(); break;
				case DELETE_CACHE_TABLES: m_ResourceManager.deleteCacheTables(); break;
				case CLEAR_CACHE: m_ResourceManager.clearCache(); break;
				case POSE_QUERY: m_ResourceManager.poseQuery((String)arg); break;
			}
		}
		catch(Exception e)
		{
			
			StdErrReporterException.reportErrorFromException(
					ProxyServer.class.getName(), 
					meth.error, e);
		}	
	}
	
	public ResourceManager getResourceManager()
	{
		return m_ResourceManager;
	}
	
	@Override
	public void run()
	{
		this.start();
		
	}
	
	ClientConnHandler 	m_ClientHandler;
	HttpMsgHandler		m_HttpMsgHandler;
	ResourceManager		m_ResourceManager;
	
	enum MethodToInvoke
	{
		CONSTRUCT_CACHE_TABLES("Unable to construct cache tables"),
		DELETE_CACHE_TABLES("Unable to delete cache tables"),
		CLEAR_CACHE("Unable to clear the cache"),
		POSE_QUERY("Unable to pose query");
		
		private MethodToInvoke(String error)
		{
			this.error = error;
		}
		final String error;
	}



}   
